package com.gitee.melin.bee.util;

import static java.util.Collections.emptyList;

import java.io.IOException;
import java.net.*;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class NetUtils {

    private static final Logger LOG = LoggerFactory.getLogger(NetUtils.class);

    // returned port range is [30000, 39999]
    private static final int RND_PORT_START = 30000;

    private static final int RND_PORT_RANGE = 10000;

    // valid port range is (0, 65535]
    private static final int MIN_PORT = 1;

    private static final int MAX_PORT = 65535;

    private static final Pattern ADDRESS_PATTERN = Pattern.compile("^\\d{1,3}(\\.\\d{1,3}){3}\\:\\d{1,5}$");

    private static final Pattern LOCAL_IP_PATTERN = Pattern.compile("127(\\.\\d{1,3}){3}$");

    private static final Pattern IP_PATTERN = Pattern.compile("\\d{1,3}(\\.\\d{1,3}){3,5}$");

    private static volatile InetAddress localAddress = null;

    private static final String LOCALHOST_VALUE = "127.0.0.1";

    private static final String LOCALHOST_KEY = "localhost";

    private static final String ANYHOST_VALUE = "0.0.0.0";

    public static final String NETWORK_IGNORED_INTERFACE = "network.interface.ignored";

    public static final String PREFERRED_NETWORK_INTERFACE = "network.interface.preferred";

    public static final String NETWORK_VPN_IP = "network.vpn.ip";

    /** store the used port. the set used only on the synchronized method. */
    private static final BitSet USED_PORT = new BitSet(65536);

    public static int getRandomPort() {
        return RND_PORT_START + ThreadLocalRandom.current().nextInt(RND_PORT_RANGE);
    }

    public static synchronized int getAvailablePort() {
        int randomPort = getRandomPort();
        return getAvailablePort(randomPort);
    }

    public static synchronized int getAvailablePort(int port) {
        if (port < MIN_PORT) {
            port = MIN_PORT;
        }
        for (int i = port; i < MAX_PORT; i++) {
            if (USED_PORT.get(i)) {
                continue;
            }
            try (ServerSocket ignored = new ServerSocket(i)) {
                USED_PORT.set(i);
                return i;
            } catch (IOException e) {
                // continue
            }
        }
        return port;
    }

    public static boolean testNetConnection(String address) {
        String url = address;
        if (StringUtils.startsWith(address, "http://")) {
            url = StringUtils.substringAfter(address, "http://");
        }

        String[] arr = StringUtils.split(url, ":");
        if (arr.length == 2 && StringUtils.isNumeric(arr[1])) {
            return testNetConnection(arr[0], Integer.parseInt(arr[1]));
        } else {
            LOG.error("address: " + address + ", right address: ip:port & http://ip:port");
            return false;
        }
    }

    public static boolean testNetConnection(String host, int port) {
        try (Socket socket = new Socket()) {
            SocketAddress socketAddress = new InetSocketAddress(host, port);
            int timeout = 2000;
            socket.connect(socketAddress, timeout);
            return true;
        } catch (IOException e) {
            LOG.debug("测试 {}: {} 连接失败，网络不通: {}", host, port, e.getMessage());
            return false;
        }
    }

    /**
     * Check the port whether is in use in os
     *
     * @param port the port to check if in use
     * @return is the given port in use or not
     */
    public static boolean isPortInUsed(int port) {
        try (ServerSocket ignored = new ServerSocket(port)) {
            return false;
        } catch (IOException e) {
            // continue
        }
        return true;
    }

    public static boolean isValidPort(int port) {
        return port > MIN_PORT && port < MAX_PORT;
    }

    public static boolean isValidAddress(String address) {
        return ADDRESS_PATTERN.matcher(address).matches();
    }

    public static boolean isLocalHost(String host) {
        return host != null && (LOCAL_IP_PATTERN.matcher(host).matches() || host.equalsIgnoreCase(LOCALHOST_KEY));
    }

    public static boolean isAnyHost(String host) {
        return ANYHOST_VALUE.equals(host);
    }

    public static boolean isInvalidLocalHost(String host) {
        return host == null
                || host.length() == 0
                || host.equalsIgnoreCase(LOCALHOST_KEY)
                || host.equals(ANYHOST_VALUE)
                || host.startsWith("127.");
    }

    public static boolean isValidLocalHost(String host) {
        return !isInvalidLocalHost(host);
    }

    public static InetSocketAddress getLocalSocketAddress(String host, int port) {
        return isInvalidLocalHost(host) ? new InetSocketAddress(port) : new InetSocketAddress(host, port);
    }

    static boolean isValidV4Address(InetAddress address) {
        if (address == null || address.isLoopbackAddress()) {
            return false;
        }

        String name = address.getHostAddress();
        return (name != null
                && IP_PATTERN.matcher(name).matches()
                && !ANYHOST_VALUE.equals(name)
                && !LOCALHOST_VALUE.equals(name));
    }

    /**
     * Check if an ipv6 address
     *
     * @return true if it is reachable
     */
    static boolean isPreferIPV6Address() {
        boolean result = false;
        try {
            result = Boolean.parseBoolean(System.getProperty("java.net.preferIPv6Addresses"));
        } catch (IllegalArgumentException | NullPointerException e) {
        }
        return result;
    }

    /**
     * normalize the ipv6 Address, convert scope name to scope id. e.g. convert
     * fe80:0:0:0:894:aeec:f37d:23e1%en0 to fe80:0:0:0:894:aeec:f37d:23e1%5
     *
     * <p>The %5 after ipv6 address is called scope id. see java doc of {@link Inet6Address} for
     * more details.
     *
     * @param address the input address
     * @return the normalized address, with scope id converted to int
     */
    static InetAddress normalizeV6Address(Inet6Address address) {
        String addr = address.getHostAddress();
        int i = addr.lastIndexOf('%');
        if (i > 0) {
            try {
                return InetAddress.getByName(addr.substring(0, i) + '%' + address.getScopeId());
            } catch (UnknownHostException e) {
                // ignore
                LOG.debug("Unknown IPV6 address: ", e);
            }
        }
        return address;
    }

    private static volatile String hostAddress;

    public static String getLocalHost() {
        String vpnIp = System.getenv(NETWORK_VPN_IP);
        if (StringUtils.isNotBlank(vpnIp)) {
            return vpnIp;
        }

        if (hostAddress != null) {
            return hostAddress;
        }

        InetAddress address = getLocalAddress();
        if (address != null) {
            return hostAddress = address.getHostAddress();
        }
        return LOCALHOST_VALUE;
    }

    /**
     * Find first valid IP from local network card
     *
     * @return first valid local IP
     */
    public static InetAddress getLocalAddress() {
        if (localAddress != null) {
            return localAddress;
        }
        InetAddress localAddress = getLocalAddress0();
        NetUtils.localAddress = localAddress;
        return localAddress;
    }

    private static Optional<InetAddress> toValidAddress(InetAddress address) {
        if (address instanceof Inet6Address) {
            Inet6Address v6Address = (Inet6Address) address;
            if (isPreferIPV6Address()) {
                return Optional.ofNullable(normalizeV6Address(v6Address));
            }
        }
        if (isValidV4Address(address)) {
            return Optional.of(address);
        }
        return Optional.empty();
    }

    private static InetAddress getLocalAddress0() {
        InetAddress localAddress = null;

        // @since 2.7.6, choose the {@link NetworkInterface} first
        try {
            NetworkInterface networkInterface = findNetworkInterface();
            Enumeration<InetAddress> addresses = networkInterface.getInetAddresses();
            while (addresses.hasMoreElements()) {
                Optional<InetAddress> addressOp = toValidAddress(addresses.nextElement());
                if (addressOp.isPresent()) {
                    try {
                        if (addressOp.get().isReachable(200)) {
                            return addressOp.get();
                        }
                    } catch (IOException e) {
                        // ignore
                    }
                }
            }
        } catch (Throwable e) {
            LOG.error(e.getMessage(), e);
        }

        try {
            localAddress = InetAddress.getLocalHost();
            Optional<InetAddress> addressOp = toValidAddress(localAddress);
            if (addressOp.isPresent()) {
                return addressOp.get();
            }
        } catch (Throwable e) {
            LOG.error(e.getMessage(), e);
        }

        return localAddress;
    }

    /**
     * Returns {@code true} if the specified {@link NetworkInterface} should be ignored with the
     * given conditions.
     *
     * @param networkInterface the {@link NetworkInterface} to check
     * @return {@code true} if the specified {@link NetworkInterface} should be ignored, otherwise
     *     {@code false}
     * @throws SocketException SocketException if an I/O error occurs.
     */
    private static boolean ignoreNetworkInterface(NetworkInterface networkInterface) throws SocketException {
        if (networkInterface == null
                || networkInterface.isLoopback()
                || networkInterface.isVirtual()
                || !networkInterface.isUp()) {
            return true;
        }
        // kubernetes docker flannel
        String ignoredInterfaces = System.getProperty(NETWORK_IGNORED_INTERFACE, "cni0,docker0,flannel.1");
        String networkInterfaceDisplayName;
        if (StringUtils.isNotEmpty(ignoredInterfaces)
                && StringUtils.isNotEmpty(networkInterfaceDisplayName = networkInterface.getDisplayName())) {
            for (String ignoredInterface : ignoredInterfaces.split(",")) {
                String trimIgnoredInterface = ignoredInterface.trim();
                boolean matched = false;
                try {
                    matched = networkInterfaceDisplayName.matches(trimIgnoredInterface);
                } catch (PatternSyntaxException e) {
                    // if trimIgnoredInterface is a invalid regular expression, a
                    // PatternSyntaxException will be thrown out
                    LOG.warn(
                            "exception occurred: " + networkInterfaceDisplayName + " matches " + trimIgnoredInterface,
                            e);
                } finally {
                    if (matched) {
                        return true;
                    }
                    if (networkInterfaceDisplayName.equals(trimIgnoredInterface)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    /**
     * Get the valid {@link NetworkInterface network interfaces}
     *
     * @return the valid {@link NetworkInterface}s
     * @throws SocketException SocketException if an I/O error occurs.
     * @since 2.7.6
     */
    private static List<NetworkInterface> getValidNetworkInterfaces() throws SocketException {
        List<NetworkInterface> validNetworkInterfaces = new LinkedList<>();
        Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
        while (interfaces.hasMoreElements()) {
            NetworkInterface networkInterface = interfaces.nextElement();
            if (ignoreNetworkInterface(networkInterface)) { // ignore
                continue;
            }
            validNetworkInterfaces.add(networkInterface);
        }
        return validNetworkInterfaces;
    }

    /**
     * Is preferred {@link NetworkInterface} or not
     *
     * @param networkInterface {@link NetworkInterface}
     * @return if the name of the specified {@link NetworkInterface} matches or <code>false</code>
     */
    public static boolean isPreferredNetworkInterface(NetworkInterface networkInterface) {
        String preferredNetworkInterface = System.getProperty(PREFERRED_NETWORK_INTERFACE);
        return Objects.equals(networkInterface.getDisplayName(), preferredNetworkInterface);
    }

    /**
     * Get the suitable {@link NetworkInterface}
     *
     * @return If no {@link NetworkInterface} is available , return <code>null</code>
     * @since 2.7.6
     */
    public static NetworkInterface findNetworkInterface() {

        List<NetworkInterface> validNetworkInterfaces = emptyList();
        try {
            validNetworkInterfaces = getValidNetworkInterfaces();
        } catch (Throwable e) {
            LOG.error(e.getMessage(), e);
        }

        NetworkInterface result = null;

        // Try to find the preferred one
        for (NetworkInterface networkInterface : validNetworkInterfaces) {
            if (isPreferredNetworkInterface(networkInterface)) {
                result = networkInterface;
                break;
            }
        }

        if (result == null) { // If not found, try to get the first one
            for (NetworkInterface networkInterface : validNetworkInterfaces) {
                Enumeration<InetAddress> addresses = networkInterface.getInetAddresses();
                while (addresses.hasMoreElements()) {
                    Optional<InetAddress> addressOp = toValidAddress(addresses.nextElement());
                    if (addressOp.isPresent()) {
                        try {
                            if (addressOp.get().isReachable(100)) {
                                return networkInterface;
                            }
                        } catch (IOException e) {
                            // ignore
                        }
                    }
                }
            }
        }

        if (result == null) {
            result = first(validNetworkInterfaces);
        }

        return result;
    }

    /**
     * @param hostName
     * @return ip address or hostName if UnknownHostException
     */
    public static String getIpByHost(String hostName) {
        try {
            return InetAddress.getByName(hostName).getHostAddress();
        } catch (UnknownHostException e) {
            return hostName;
        }
    }

    public static <T> T first(Collection<T> values) {
        if (values == null || values.isEmpty()) {
            return null;
        } else if (values instanceof List) {
            List<T> list = (List) values;
            return list.get(0);
        } else {
            return values.iterator().next();
        }
    }

    public static int getAvailablePort(String bindPortRange) {
        Iterator<Integer> portsIterator = NetUtils.getPortRangeFromString(bindPortRange);
        int chosenPort = 0;
        while (portsIterator.hasNext()) {
            chosenPort = portsIterator.next();
            if (NetUtils.isTcpPortAvailable(chosenPort)) {
                break;
            }
        }
        LOG.info("chosen port: {}, bindPortRange: {}", chosenPort, bindPortRange);
        return chosenPort;
    }

    /**
     * Returns an iterator over available ports defined by the range definition.
     *
     * @param rangeDefinition String describing a single port, a range of ports or multiple ranges.
     * @return Set of ports from the range definition
     * @throws NumberFormatException If an invalid string is passed.
     */
    public static Iterator<Integer> getPortRangeFromString(String rangeDefinition) throws NumberFormatException {
        final String[] ranges = rangeDefinition.trim().split(",");

        UnionIterator<Integer> iterators = new UnionIterator<>();

        for (String rawRange : ranges) {
            Iterator<Integer> rangeIterator;
            String range = rawRange.trim();
            int dashIdx = range.indexOf('-');
            if (dashIdx == -1) {
                // only one port in range:
                final int port = Integer.valueOf(range);
                if (!isValidPort(port)) {
                    throw new IllegalArgumentException(
                            "Invalid port configuration. Port must be between 0 and 65535, but was " + port + ".");
                }
                rangeIterator = Collections.singleton(Integer.valueOf(range)).iterator();
            } else {
                // evaluate range
                final int start = Integer.valueOf(range.substring(0, dashIdx));
                if (!isValidPort(start)) {
                    throw new IllegalArgumentException(
                            "Invalid port configuration. Port must be between 0 and 65535, but was " + start + ".");
                }
                final int end = Integer.valueOf(range.substring(dashIdx + 1, range.length()));
                if (!isValidPort(end)) {
                    throw new IllegalArgumentException(
                            "Invalid port configuration. Port must be between 0 and 65535, but was " + end + ".");
                }
                rangeIterator = new Iterator<Integer>() {
                    int i = start;

                    @Override
                    public boolean hasNext() {
                        return i <= end;
                    }

                    @Override
                    public Integer next() {
                        return i++;
                    }

                    @Override
                    public void remove() {
                        throw new UnsupportedOperationException("Remove not supported");
                    }
                };
            }
            iterators.add(rangeIterator);
        }

        return iterators;
    }

    /**
     * Checks to see if a specific port is available.
     *
     * @param port the port to check for availability
     */
    public static boolean isTcpPortAvailable(int port) {
        if (port < MIN_PORT || port > MAX_PORT) {
            throw new IllegalArgumentException("Invalid start port: " + port);
        }

        ServerSocket ss = null;
        DatagramSocket ds = null;
        try {
            ss = new ServerSocket(port);
            ss.setReuseAddress(true);
            ds = new DatagramSocket(port);
            ds.setReuseAddress(true);
            return true;
        } catch (IOException e) {
        } finally {
            if (ds != null) {
                ds.close();
            }

            if (ss != null) {
                try {
                    ss.close();
                } catch (IOException e) {
                    /* should not be thrown */
                }
            }
        }

        return false;
    }

    public static void main(String[] args) {
        testNetConnection("http://cdh3:45929");
    }
}
